package ns

import (
	"GLibHac/fssystem"
	"fmt"
)

type FragmentMapEntry struct {
	ContentIndex  int16
	FragmentIndex int16
}

func NewFragmentMapEntry(reader fssystem.IStorage) (r *FragmentMapEntry, err error) {
	r = &FragmentMapEntry{}
	r.ContentIndex, err = reader.ReadInt16()
	if err != nil {
		return nil, err
	}
	r.FragmentIndex, err = reader.ReadInt16()
	if err != nil {
		return nil, err
	}
	return
}

type CnmtPrevContent struct {
	NcaId []byte
	Size  int64
	Type  ContentType
}

func NewCnmtPrevContent(reader fssystem.IStorage) (r *CnmtPrevContent, err error) {
	r = &CnmtPrevContent{}
	r.NcaId, err = reader.ReadBytes(0x10)
	if err != nil {
		return nil, err
	}
	readUint32, err := reader.ReadUint32()
	if err != nil {
		return r, err
	}
	r.Size = int64(readUint32)
	readUint16, err := reader.ReadUint16()
	if err != nil {
		return nil, err
	}
	r.Size |= int64(readUint16) << 32
	readByte, err := reader.ReadByte()
	if err != nil {
		return nil, err
	}
	r.Type = ContentType(readByte)
	err = reader.SetPosition(reader.Position() + 1)
	if err != nil {
		return nil, err
	}
	return
}

type UpdateType byte

func (a UpdateType) String() string {
	names := []string{
		"ApplyAsDelta",
		"Overwrite",
		"Create",
	}
	return names[a]
}

const (
	UpdateTypeApplyAsDelta = UpdateType(iota)
	UpdateTypeOverwrite
	UpdateTypeCreate
)

type CnmtFragmentSetInfo struct {
	NcaIdOld               []byte
	NcaIdNew               []byte
	SizeOld                int64
	SizeNew                int64
	FragmentCount          int16
	Type                   ContentType
	DeltaType              UpdateType
	FragmentSetInfoField30 int32
}

func NewCnmtFragmentSetInfo(reader fssystem.IStorage) (r *CnmtFragmentSetInfo, err error) {
	r = &CnmtFragmentSetInfo{}
	r.NcaIdOld, err = reader.ReadBytes(0x10)
	r.NcaIdNew, err = reader.ReadBytes(0x10)
	readUint32, err := reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	r.SizeOld = int64(readUint32)
	readUint16, err := reader.ReadUint16()
	if err != nil {
		return nil, err
	}
	r.SizeOld |= int64(readUint16) << 32
	readUint16, err = reader.ReadUint16()
	if err != nil {
		return nil, err
	}
	r.SizeNew |= int64(readUint16) << 32
	readUint32, err = reader.ReadUint32()
	r.SizeNew = int64(readUint32)
	r.FragmentCount, err = reader.ReadInt16()
	if err != nil {
		return nil, err
	}
	readByte, err := reader.ReadByte()
	if err != nil {
		return nil, err
	}
	r.Type = ContentType(readByte)
	readByte, err = reader.ReadByte()
	if err != nil {
		return nil, err
	}
	r.DeltaType = UpdateType(readByte)
	r.FragmentSetInfoField30, err = reader.ReadInt()
	if err != nil {
		return nil, err
	}
	return
}

type CnmtDeltaSetInfo struct {
	TitleIdOld        uint64
	TitleIdNew        uint64
	VersionOld        *TitleVersion
	VersionNew        *TitleVersion
	FragmentSetCount  int64
	DeltaContentCount int64
}

func NewCnmtDeltaSetInfo(reader fssystem.IStorage) (r *CnmtDeltaSetInfo, err error) {
	r = &CnmtDeltaSetInfo{}
	r.TitleIdOld, err = reader.ReadUint64()
	if err != nil {
		return nil, err
	}
	r.TitleIdNew, err = reader.ReadUint64()
	if err != nil {
		return nil, err
	}
	version, err := reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	r.VersionOld = NewTitleVersion(version, false)
	version, err = reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	r.VersionNew = NewTitleVersion(version, false)
	r.FragmentSetCount, err = reader.ReadInt64()
	if err != nil {
		return nil, err
	}
	r.DeltaContentCount, err = reader.ReadInt64()
	return
}

type CnmtPrevDelta struct {
	TitleIdOld           uint64
	TitleIdNew           uint64
	VersionOld           *TitleVersion
	VersionNew           *TitleVersion
	Size                 int64
	CnmtPrevDeltaField20 int64
}

func NewCnmtPrevDelta(reader fssystem.IStorage) (r *CnmtPrevDelta, err error) {
	r = &CnmtPrevDelta{}
	r.TitleIdOld, err = reader.ReadUint64()
	r.TitleIdNew, err = reader.ReadUint64()
	version, err := reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	r.VersionOld = NewTitleVersion(version, false)
	version, err = reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	r.VersionNew = NewTitleVersion(version, false)
	r.Size, err = reader.ReadInt64()
	if err != nil {
		return nil, err
	}
	r.CnmtPrevDeltaField20, err = reader.ReadInt64()
	return r, err
}

type CnmtPrevMetaEntry struct {
	TitleId                  uint64
	Version                  *TitleVersion
	Type                     ContentMetaType
	Hash                     []byte
	ContentCount             int16
	CnmtPrevMetaEntryField32 int16
	CnmtPrevMetaEntryField34 int32
}

func NewCnmtPrevMetaEntry(reader fssystem.IStorage) (r *CnmtPrevMetaEntry, err error) {
	r = &CnmtPrevMetaEntry{}
	r.TitleId, err = reader.ReadUint64()
	readUint32, err := reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	r.Version = NewTitleVersion(readUint32, false)
	readByte, err := reader.ReadByte()
	if err != nil {
		return nil, err
	}
	r.Type = ContentMetaType(readByte)
	err = reader.SetPosition(reader.Position() + 3)
	if err != nil {
		return nil, err
	}
	r.Hash, err = reader.ReadBytes(0x20)
	r.ContentCount, err = reader.ReadInt16()
	r.CnmtPrevMetaEntryField32, err = reader.ReadInt16()
	if err != nil {
		return
	}
	r.CnmtPrevMetaEntryField34, err = reader.ReadInt()
	return
}

type CnmtExtended struct {
	PrevMetaCount     int32
	PrevDeltaSetCount int32
	DeltaSetCount     int32
	FragmentSetCount  int32
	PrevContentCount  int32
	DeltaContentCount int32
	PrevMetas         []*CnmtPrevMetaEntry
	PrevDeltaSets     []*CnmtPrevDelta
	DeltaSets         []*CnmtDeltaSetInfo
	FragmentSets      []*CnmtFragmentSetInfo
	PrevContents      []*CnmtPrevContent
	DeltaContents     []*CnmtContentEntry
	FragmentMap       []*FragmentMapEntry
}

func NewCnmtExtended(reader fssystem.IStorage) (r *CnmtExtended, err error) {
	r = &CnmtExtended{}
	r.PrevMetaCount, err = reader.ReadInt()     // Lists all previous content meta files
	r.PrevDeltaSetCount, err = reader.ReadInt() // Lists all previous delta sets
	r.DeltaSetCount, err = reader.ReadInt()     // Lists the delta set for the current title version
	r.FragmentSetCount, err = reader.ReadInt()  // Groups fragments into full deltas
	r.PrevContentCount, err = reader.ReadInt()  // Lists all previous NCAs for the title
	r.DeltaContentCount, err = reader.ReadInt() // Lists all NCAs containing delta fragments
	err = reader.SetPosition(reader.Position() + 4)

	r.PrevMetas = make([]*CnmtPrevMetaEntry, r.PrevMetaCount)
	r.PrevDeltaSets = make([]*CnmtPrevDelta, r.PrevDeltaSetCount)
	r.DeltaSets = make([]*CnmtDeltaSetInfo, r.DeltaSetCount)
	r.FragmentSets = make([]*CnmtFragmentSetInfo, r.FragmentSetCount)
	r.PrevContents = make([]*CnmtPrevContent, r.PrevContentCount)
	r.DeltaContents = make([]*CnmtContentEntry, r.DeltaContentCount)

	for i := int32(0); i < r.PrevMetaCount; i++ {
		r.PrevMetas[i], err = NewCnmtPrevMetaEntry(reader)
		if err != nil {
			return nil, err
		}
	}

	for i := int32(0); i < r.PrevDeltaSetCount; i++ {
		r.PrevDeltaSets[i], err = NewCnmtPrevDelta(reader)
		if err != nil {
			return nil, err
		}
	}

	for i := int32(0); i < r.DeltaSetCount; i++ {
		r.DeltaSets[i], err = NewCnmtDeltaSetInfo(reader)
		if err != nil {
			return nil, err
		}
	}
	var fragmentCount = int32(0)
	for i := int32(0); i < r.FragmentSetCount; i++ {
		r.FragmentSets[i], err = NewCnmtFragmentSetInfo(reader)
		if err != nil {
			return nil, err
		}
		fragmentCount += int32(r.FragmentSets[i].FragmentCount)
	}

	for i := int32(0); i < r.PrevContentCount; i++ {
		r.PrevContents[i], err = NewCnmtPrevContent(reader)
		if err != nil {
			return nil, err
		}
	}

	for i := int32(0); i < r.DeltaContentCount; i++ {
		r.DeltaContents[i], err = NewCnmtContentEntry(reader)
		if err != nil {
			return nil, err
		}
	}
	r.FragmentMap = make([]*FragmentMapEntry, fragmentCount)

	for i := int32(0); i < fragmentCount; i++ {
		r.FragmentMap[i], err = NewFragmentMapEntry(reader)
		if err != nil {
			return nil, err
		}
	}
	return
}

type CnmtContentMetaEntry struct {
	TitleId uint64
	Version *TitleVersion
	Type    ContentType
}

func NewCnmtContentMetaEntry(reader fssystem.IStorage) (r *CnmtContentMetaEntry, err error) {
	r = &CnmtContentMetaEntry{}
	r.TitleId, err = reader.ReadUint64()
	version, err := reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	r.Version = NewTitleVersion(version, true)
	readByte, err := reader.ReadByte()
	if err != nil {
		return nil, err
	}
	r.Type = ContentType(readByte)
	err = reader.SetPosition(reader.Position() + 3)
	return
}

type ContentType byte

func (c ContentType) String() string {
	names := []string{
		"Meta",
		"Program",
		"Data",
		"Control",
		"HtmlDocument",
		"LegalInformation",
		"DeltaFragment",
	}
	return names[c]
}

const (
	ContentTypeMeta = ContentType(iota)
	ContentTypeProgram
	ContentTypeData
	ContentTypeControl
	ContentTypeHtmlDocument
	ContentTypeLegalInformation
	ContentTypeDeltaFragment
)

type CnmtContentEntry struct {
	Hash  []byte
	NcaId []byte
	Size  int64
	Type  ContentType
}

func NewCnmtContentEntry(reader fssystem.IStorage) (r *CnmtContentEntry, err error) {
	r = &CnmtContentEntry{}
	r.Hash, err = reader.ReadBytes(0x20)
	if err != nil {
		return nil, err
	}
	r.NcaId, err = reader.ReadBytes(0x10)
	if err != nil {
		return nil, err
	}
	readUint32, err := reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	r.Size = int64(readUint32)
	readUint16, err := reader.ReadUint16()
	if err != nil {
		return nil, err
	}
	r.Size |= int64(readUint16) << 32
	readByte, err := reader.ReadByte()
	if err != nil {
		return nil, err
	}
	r.Type = ContentType(readByte)
	err = reader.SetPosition(reader.Position() + 1)
	if err != nil {
		return nil, err
	}
	return
}

type TitleVersion struct {
	Version  uint32
	Major    int
	Minor    int
	Patch    int
	Revision int
}

func (s *TitleVersion) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", s.Major, s.Minor, s.Patch, s.Revision)
}

func NewTitleVersion(version uint32, isSystemTitle bool) (r *TitleVersion) {
	r = &TitleVersion{}
	r.Version = version
	if isSystemTitle {
		r.Revision = (int)(version & ((1 << 16) - 1))
		r.Patch = (int)((version >> 16) & ((1 << 4) - 1))
		r.Minor = (int)((version >> 20) & ((1 << 6) - 1))
		r.Major = (int)((version >> 26) & ((1 << 6) - 1))
	} else {
		r.Revision = int(byte(version))
		r.Patch = int(byte(version >> 8))
		r.Minor = int(byte(version >> 16))
		r.Major = int(byte(version >> 24))
	}
	return
}

type Cnmt struct {
	Hash                      []byte
	ContentEntries            []*CnmtContentEntry
	MetaEntries               []*CnmtContentMetaEntry
	ExtendedData              *CnmtExtended
	MinimumApplicationVersion *TitleVersion
	MinimumSystemVersion      *TitleVersion
	PatchTitleId              uint64
	ApplicationTitleId        uint64
	TitleId                   uint64
	TitleVersion              *TitleVersion
	Type                      ContentMetaType
	FieldD                    byte
	TableOffset               int
	ContentEntryCount         int
	MetaEntryCount            int
}

func NewCnmt(reader fssystem.IStorage) (r *Cnmt, err error) {
	r = &Cnmt{}
	r.TitleId, err = reader.ReadUint64()
	if err != nil {
		return nil, err
	}
	version, err := reader.ReadUint32()
	if err != nil {
		return nil, err
	}
	readByte, err := reader.ReadByte()
	if err != nil {
		return nil, err
	}
	r.Type = ContentMetaType(readByte)
	r.TitleVersion = NewTitleVersion(version, r.Type < ContentMetaTypeApplication)
	r.FieldD, err = reader.ReadByte()
	if err != nil {
		return nil, err
	}
	readUint16, err := reader.ReadUint16()
	if err != nil {
		return nil, err
	}
	r.TableOffset = int(readUint16)
	readUint16, err = reader.ReadUint16()
	if err != nil {
		return nil, err
	}
	r.ContentEntryCount = int(readUint16)
	readUint16, err = reader.ReadUint16()
	if err != nil {
		return nil, err
	}
	r.MetaEntryCount = int(readUint16)

	// Old, pre-release cnmt files don't have the "required system version" field.
	// Try to detect this by reading the padding after that field.
	// The old format usually contains hashes there.
	_, err = reader.ReadBytes(8)
	if err != nil {
		return nil, err
	}
	var padding int32
	padding, err = reader.ReadInt()
	if err != nil {
		return nil, err
	}
	isOldCnmtFormat := padding != 0

	switch r.Type {
	case ContentMetaTypeApplication:
		r.ApplicationTitleId = r.TitleId
		r.PatchTitleId, err = reader.ReadUint64()
		readUint32, err := reader.ReadUint32()
		if err != nil {
			return nil, err
		}
		r.MinimumSystemVersion = NewTitleVersion(readUint32, true)
	case ContentMetaTypePatch:
		r.ApplicationTitleId, err = reader.ReadUint64()
		if err != nil {
			return nil, err
		}
		readUint32, err := reader.ReadUint32()
		if err != nil {
			return nil, err
		}
		r.MinimumSystemVersion = NewTitleVersion(readUint32, true)
	case ContentMetaTypeAddOnContent:
		r.ApplicationTitleId, err = reader.ReadUint64()
		if err != nil {
			return nil, err
		}
		readUint32, err := reader.ReadUint32()
		if err != nil {
			return nil, err
		}
		r.MinimumApplicationVersion = NewTitleVersion(readUint32, false)
	}

	baseOffset := 0x20
	if isOldCnmtFormat {
		baseOffset = 0x18
	}
	err = reader.SetPosition(int64(baseOffset) + int64(r.TableOffset))
	if err != nil {
		return nil, err
	}

	r.ContentEntries = make([]*CnmtContentEntry, r.ContentEntryCount)
	r.MetaEntries = make([]*CnmtContentMetaEntry, r.MetaEntryCount)

	for i := 0; i < r.ContentEntryCount; i++ {
		r.ContentEntries[i], err = NewCnmtContentEntry(reader)
		if err != nil {
			return nil, err
		}
	}

	for i := 0; i < r.MetaEntryCount; i++ {
		r.MetaEntries[i], err = NewCnmtContentMetaEntry(reader)
		if err != nil {
			return nil, err
		}
	}

	if r.Type == ContentMetaTypePatch {
		r.ExtendedData, err = NewCnmtExtended(reader)
		if err != nil {
			return nil, err
		}
	}

	r.Hash, err = reader.ReadBytes(0x20)
	if err != nil {
		return nil, err
	}
	return
}
